{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit ConsoleManager;

interface

uses Classes, TextEntry, UIObjects;

const
  HISTORY_FILE = 'ConsoleHistory.txt';

  CONSOLE_PROMPT = 'Command: ';
  CONSOLE_LINES = 32; // Maximum number of lines that will fit onscreen
  CONSOLE_LINE_LEN = 66; // Maximum chars that will fit in one line onscreen
  CONSOLE_WIDE_LEN = 82; // Maximum chars that will fit in one line widescreen
  CONSOLE_HISTORY = 300; // Maximum number of lines to keep in our history
  CONSOLE_PAGE_SIZE = 5; // Number of lines we page up/down by when scrolling

  CONSOLE_MSG_UNKNOWN_CMD = 'Unknown command! (Type HELP for a list of commands)';
  CONSOLE_MSG_HELP = 'mediate console help.' + sLineBreak + sLineBreak +
                     'Supported commands:' + sLineBreak +
                     'HELP     - show this help' + sLineBreak +
                     'VERSION  - show mediate version information' + sLineBreak +
                     'PREFS    - show mediate preference settings' + sLineBreak +
                     'LISTMETA - show mediate metadata values' + sLineBreak +
                     'LISTOBJ  - show list of objects of specified type' + sLineBreak +
                     'DUMPOBJ  - show properties of specified object' + sLineBreak +
                     'FN       - execute requested theme function' + sLineBreak +
                     'UPTIME   - show length of time mediate has been running for' + sLineBreak +
                     'DIAG     - show diagnostic information' + sLineBreak +
                     'LOG      - show or change current logging level' + sLineBreak +
                     'CLS      - clear console display history' + sLineBreak +
                     'EXIT     - exit mediate console' + sLineBreak +
                     'RESTART  - restart mediate user interface' + sLineBreak +
                     'QUIT     - quit mediate user interface' + sLineBreak + sLineBreak +
                     'Supported watches:' + sLineBreak +
                     'SHOWFPS/HIDEFPS - show/hide frames per second display' + sLineBreak +
                     'SHOWDUR/HIDEDUR - show/hide frame render duration display' + sLineBreak +
                     'SHOWMETA/HIDEMETA - show/hide meta data value(s) display' + sLineBreak +
                     'SHOWPROF/HIDEPROF - show/hide profiling diagnostic display';
  CONSOLE_MSG_LOG  = 'Usage: LOG [level] - where level may be DEBUG, INFO, WARN or ERROR';
  CONSOLE_MSG_META = 'Usage: LISTMETA [prefix] - where prefix is metadata name prefix';
  CONSOLE_MSG_FN   = 'Usage: FN [name] [target] [parameter] - where name is required' + sLineBreak +
                     'function, target is function target and parameter is function' + sLineBreak +
                     'parameter';
  CONSOLE_MSG_LIST = 'Usage: LISTOBJ [type] - where type may be COLOUR, FILL, PLASMA,' + sLineBreak +
                     'FONT, BITMAP, BACKGROUND, FOREGROUND, SHAPE, CAPTION, MEDIAWINDOW,' + sLineBreak +
                     'BAR, BUTTON, MEDIAGRID, TEXTEDIT, EFFECT, PLUGINWINDOW, FUNCTION,' + sLineBreak +
                     'EXPRESSION, MACRO, ACTION, TIMER, METADATA, PLACE, ANIMATOR,' + sLineBreak +
                     'LAYOUT, CONTROL, CONTAINER, SCREEN, SOUND, LANGUAGE';
  CONSOLE_MSG_DUMP = 'Usage: DUMPOBJ [type] [name] - for types see LISTOBJ command';

  CONSOLE_CMD_HELP     = 'HELP';
  CONSOLE_CMD_DIAG     = 'DIAG';
  CONSOLE_CMD_LOG      = 'LOG';
  CONSOLE_CMD_VER      = 'VERSION';
  CONSOLE_CMD_PREFS    = 'PREFS';
  CONSOLE_CMD_LISTMETA = 'LISTMETA';
  CONSOLE_CMD_LISTOBJ  = 'LISTOBJ';
  CONSOLE_CMD_DUMPOBJ  = 'DUMPOBJ';
  CONSOLE_CMD_UPTIME   = 'UPTIME';
  CONSOLE_CMD_FN       = 'FN';
  CONSOLE_CMD_CLS      = 'CLS';
  CONSOLE_CMD_EXIT     = 'EXIT';
  CONSOLE_CMD_RESTART  = 'RESTART';
  CONSOLE_CMD_QUIT     = 'QUIT';
  CONSOLE_CMD_BYE      = 'BYE'; // undocumented - unpredictable results
  CONSOLE_CMD_EXAMPLE  = 'EXAMPLE'; // undocumented - just to show param parsing
  CONSOLE_CMD_SHOWFPS  = 'SHOWFPS';
  CONSOLE_CMD_HIDEFPS  = 'HIDEFPS';
  CONSOLE_CMD_SHOWDUR  = 'SHOWDUR';
  CONSOLE_CMD_HIDEDUR  = 'HIDEDUR';
  CONSOLE_CMD_SHOWMETA = 'SHOWMETA';
  CONSOLE_CMD_HIDEMETA = 'HIDEMETA';
  CONSOLE_CMD_SHOWDBUG = 'SHOWDEBUG';
  CONSOLE_CMD_HIDEDBUG = 'HIDEDEBUG';
  CONSOLE_CMD_SHOWPROF = 'SHOWPROF';
  CONSOLE_CMD_HIDEPROF = 'HIDEPROF';

type
  TAHMWatchType = (wtFPS, wtFrameDur, wtMeta, wtProfile, wtDebug);
  TAHMWatchTypes = set of TAHMWatchType;

  TAHMConsoleManager = class;

  TAHMConsoleCommandLine = class(TAHMTextEntryParser)
  private
    FConsole: TAHMConsoleManager;
    FParamParser: TStringList;
    FCommandIndex: Integer;
    FCommandHistory: TStringList;
    FHistoryLoaded: Boolean;
  protected
    procedure LoadHistory;
    procedure SaveHistory;
    procedure CursorUp; override;
    procedure CursorDown; override;
    procedure ScrollUp; override;
    procedure ScrollDown; override;
    procedure Enter; override;
    procedure CommandComplete; override;
  public
    constructor Create;
    destructor Destroy; override;
    property Console: TAHMConsoleManager read FConsole write FConsole;
    property CommandHistory: TStringList read FCommandHistory;
  end;

  TAHMConsoleManager = class
  private
    FWorkBuffer: TStringList;
    FOutputBuffer: TStringList;
    FDisplayBuffer: TStringList;
    FCommandLine: TAHMConsoleCommandLine;
    FLastLine: Integer;
    FWatches: TAHMWatchTypes;
    FMetaWatches: TStringList;
    FScrolling: Boolean;
    FLogging: Boolean;
    FDebugStrings: TStringList;
    procedure SetLogging(Enable: Boolean);
    function GetCommandHistory: TStringList;
    function GetCommandLine: String;
    function GetCommandOutput: String;
    function GetUnwrappedOutput: String;
    function BuildCommandOutput(Wrapped: Boolean): String;
    function GetWatchesEnabled: Boolean;
    function GetWatchesOutput: String;
    function GetDiagnosticText: String;
  protected
    procedure ScrollUp;
    procedure ScrollDown;
    procedure ScrollCancel;
    procedure ClearOutput;
    procedure Output(Text: String);
    procedure ShowHelp;
    procedure ShowDiagnostics;
    procedure ShowLog(Params: TStrings);
    procedure ShowVersion;
    procedure ShowPreferences;
    procedure ShowMeta(Params: TStrings);
    procedure ShowObjects(Params: TStrings);
    procedure DumpObject(Params: TStrings);
    function GetObjectList(ObjectType: String): TAHMUIObjectList;
    procedure ShowUpTime;
    procedure ShowUnknown;
    procedure CmdFunction(Params: TStrings);
    procedure CmdClear;
    procedure CmdExit;
    procedure CmdRestart;
    procedure CmdQuit;
    procedure CmdBye;
    procedure CmdExample(Params: TStrings);
    procedure AddWatch(WatchType: TAHMWatchType); overload;
    procedure AddWatch(WatchType: TAHMWatchType; Params: TStrings); overload;
    procedure RemoveWatch(WatchType: TAHMWatchType); overload;
    procedure RemoveWatch(WatchType: TAHMWatchType; Params: TStrings); overload;
  public
    constructor Create;
    destructor Destroy; override;
    property OutputBuffer: TStringList read FOutputBuffer;
    property EnableLogging: Boolean read FLogging write SetLogging;
    property CommandHistory: TStringList read GetCommandHistory;
    property CommandLine: String read GetCommandLine;
    property CommandOutput: String read GetCommandOutput;
    property UnwrappedOutput: String read GetUnwrappedOutput;
    property WatchesEnabled: Boolean read GetWatchesEnabled;
    property WatchesOutput: String read GetWatchesOutput;
    property DebugStrings: TStringList read FDebugStrings;
    function ProcessKeyPress(Key: Word; Shift: TShiftState): Boolean;
    procedure ProcessCommand(Command: String);
  end;

implementation

uses Windows, Forms, SysUtils, ApplicationManager, Screen,Functions, Utilities,
     Timing, Logging, AHMTypes;

//----------------------------------------------------------------------------//

constructor TAHMConsoleCommandLine.Create;
begin
  inherited Create;

  FCommandIndex := -1;
  FCommandHistory := TStringList.Create;
  FParamParser := TStringList.Create;
  FParamParser.Delimiter := ' ';
end;

destructor TAHMConsoleCommandLine.Destroy;
begin
  SaveHistory;
  FCommandHistory.Free;
  FParamParser.Free;

  inherited Destroy;
end;

procedure TAHMConsoleCommandLine.LoadHistory;
begin
  // We only need to load command history once
  if FHistoryLoaded then Exit else FHistoryLoaded := True;
  try
    // Load history - ignore any exceptions raised (not that important)
    with ApplicationManagerInstance do
      if PreferencesManager.SaveConsoleHistory then
        FCommandHistory.LoadFromFile(ConfigPath + HISTORY_FILE);
  except
    FCommandHistory.Clear;
  end;
end;

procedure TAHMConsoleCommandLine.SaveHistory;
begin
  // Only save command history if previously loaded
  if not FHistoryLoaded then Exit;
  try
    // Save history
    with ApplicationManagerInstance do
      if PreferencesManager.SaveConsoleHistory then
        FCommandHistory.SaveToFile(ConfigPath + HISTORY_FILE);
  except
    // Ignore exception
  end;
end;

procedure TAHMConsoleCommandLine.CursorUp;
begin
  // Recall command history when first requested
  LoadHistory;

  // Recall previous command in history
  if FCommandHistory.Count = 0 then Exit
  else if FCommandIndex < 0 then FCommandIndex := Pred(FCommandHistory.Count)
  else if FCommandIndex > 0 then Dec(FCommandIndex);
  Text := FCommandHistory[FCommandIndex];
end;

procedure TAHMConsoleCommandLine.CursorDown;
begin
  // Recall next command in history
  if FCommandHistory.Count = 0 then Exit
  else if FCommandIndex < 0 then FCommandIndex := Pred(FCommandHistory.Count)
  else if FCommandIndex < Pred(FCommandHistory.Count) then
  begin
    Inc(FCommandIndex);
    Text := FCommandHistory[FCommandIndex];
  end
  else
  begin
    FCommandIndex := -1;
    Text := '';
  end;
end;

procedure TAHMConsoleCommandLine.ScrollUp;
begin
  FConsole.ScrollUp;
end;

procedure TAHMConsoleCommandLine.ScrollDown;
begin
  FConsole.ScrollDown;
end;

procedure TAHMConsoleCommandLine.Enter;
var
  CmdLine: String;
begin
  // Recall command history before we parse command
  LoadHistory;

  // Parse and clear command line
  CmdLine := Trim(Text);
  FParamParser.DelimitedText := CmdLine;
  Clear;

  // Ensure we have at least one parsed paran
  if FParamParser.Count = 0 then
  begin
    FConsole.ScrollCancel;
    Exit;
  end;

  // Add unique entered text to command history
  FCommandIndex := FCommandHistory.IndexOf(CmdLine);
  if FCommandIndex < 0 then FCommandHistory.Add(CmdLine)
  else if FCommandIndex < Pred(FCommandHistory.Count) then Inc(FCommandIndex)
  else FCommandIndex := -1;

  // Split the command line into command name and its params
  CmdLine := UpperCase(FParamParser[0]);
  FParamParser.Delete(0);

  // Check entered command
  if CmdLine = CONSOLE_CMD_HELP then FConsole.ShowHelp
  else if CmdLine = CONSOLE_CMD_DIAG then FConsole.ShowDiagnostics
  else if CmdLine = CONSOLE_CMD_LOG then FConsole.ShowLog(FParamParser)
  else if CmdLine = CONSOLE_CMD_VER then FConsole.ShowVersion
  else if CmdLine = CONSOLE_CMD_PREFS then FConsole.ShowPreferences
  else if CmdLine = CONSOLE_CMD_LISTMETA then FConsole.ShowMeta(FParamParser)
  else if CmdLine = CONSOLE_CMD_LISTOBJ then FConsole.ShowObjects(FParamParser)
  else if CmdLine = CONSOLE_CMD_DUMPOBJ then FConsole.DumpObject(FParamParser)
  else if CmdLine = CONSOLE_CMD_UPTIME then FConsole.ShowUpTime
  else if CmdLine = CONSOLE_CMD_FN then FConsole.CmdFunction(FParamParser)
  else if CmdLine = CONSOLE_CMD_CLS then FConsole.CmdClear
  else if CmdLine = CONSOLE_CMD_EXIT then FConsole.CmdExit
  else if CmdLine = CONSOLE_CMD_RESTART then FConsole.CmdRestart
  else if CmdLine = CONSOLE_CMD_QUIT then FConsole.CmdQuit
  else if CmdLine = CONSOLE_CMD_BYE then FConsole.CmdBye
  else if CmdLine = CONSOLE_CMD_EXAMPLE then FConsole.CmdExample(FParamParser)
  else if CmdLine = CONSOLE_CMD_SHOWFPS then FConsole.AddWatch(wtFPS)
  else if CmdLine = CONSOLE_CMD_SHOWDUR then FConsole.AddWatch(wtFrameDur)
  else if CmdLine = CONSOLE_CMD_HIDEFPS then FConsole.RemoveWatch(wtFPS)
  else if CmdLine = CONSOLE_CMD_HIDEDUR then FConsole.RemoveWatch(wtFrameDur)
  else if CmdLine = CONSOLE_CMD_SHOWMETA then FConsole.AddWatch(wtMeta, FParamParser)
  else if CmdLine = CONSOLE_CMD_HIDEMETA then FConsole.RemoveWatch(wtMeta, FParamParser)
  else if CmdLine = CONSOLE_CMD_SHOWDBUG then FConsole.AddWatch(wtDebug)
  else if CmdLine = CONSOLE_CMD_HIDEDBUG then FConsole.RemoveWatch(wtDebug)
  else if CmdLine = CONSOLE_CMD_SHOWPROF then FConsole.AddWatch(wtProfile)
  else if CmdLine = CONSOLE_CMD_HIDEPROF then FConsole.RemoveWatch(wtProfile)
  else FConsole.ShowUnknown;
end;

procedure TAHMConsoleCommandLine.CommandComplete;
begin
//TODO Tab key is handled now so we can implement command completion
  SetText(Text + 'tab');
end;

//----------------------------------------------------------------------------//

constructor TAHMConsoleManager.Create;
begin
  inherited Create;

  FWorkBuffer := TStringList.Create;
  FOutputBuffer := TStringList.Create;
  FDisplayBuffer := TStringList.Create;
  FMetaWatches := TStringList.Create;
  FDebugStrings := TStringList.Create;
  FCommandLine := TAHMConsoleCommandLine.Create;
  FCommandLine.Console := Self;

  // Sort our list of unique meta watches
  FMetaWatches.Sorted := True;
  FMetaWatches.Duplicates := dupIgnore;

  // Initialise rendering text position
  FLastLine := 0;

  // Enable logging output to our console
  SetLogging(True);
end;

destructor TAHMConsoleManager.Destroy;
begin
  // Disconnect logging output
  SetLogging(False);

  FWorkBuffer.Free;
  FOutputBuffer.Free;
  FDisplayBuffer.Free;
  FMetaWatches.Free;
  FDebugStrings.Free;
  FCommandLine.Free;

  inherited Destroy;
end;

procedure TAHMConsoleManager.SetLogging(Enable: Boolean);
begin
  // Enable/disable shadow log for output to console
  FLogging := Enable;
  LoggingInstance.ShadowLogging := Enable;
end;

function TAHMConsoleManager.GetCommandHistory: TStringList;
begin
  Result := FCommandLine.CommandHistory;
end;

function TAHMConsoleManager.GetCommandLine: String;
begin
  Result := CONSOLE_PROMPT + FCommandLine.DisplayText;
end;

function TAHMConsoleManager.GetCommandOutput: String;
begin
  Result := BuildCommandOutput(True);
end;

function TAHMConsoleManager.GetUnwrappedOutput: String;
begin
  Result := BuildCommandOutput(False);
end;

function TAHMConsoleManager.BuildCommandOutput(Wrapped: Boolean): String;
var
  i, j, LineLen, LineCount, ScrollSpeed: Integer;
begin
  // Determine maximum line length for current screen aspect ratio
  if ScreenInstance.WideScreen then LineLen := CONSOLE_WIDE_LEN
                               else LineLen := CONSOLE_LINE_LEN;

  // Add any outstanding log messages to our output buffer
  if FLogging then LoggingInstance.ReadShadowLog(FOutputBuffer);

  // Unless we're scrolling truncate output history
  while (not FScrolling) and (FOutputBuffer.Count > CONSOLE_HISTORY) do
  begin
    FOutputBuffer.Delete(0);
    if FLastLine > 0 then Dec(FLastLine);
  end;

  // Keep a count of how many lines of text we add, including wrapped lines
  LineCount := 0;
  i := FLastLine;

  FDisplayBuffer.Clear;

  while i < FOutputBuffer.Count do
  begin
    // Have we pre-processed this line yet?
    if FOutputBuffer.Objects[i] = nil then
    begin
      // Parse line breaks into separate lines & mark source line as processed
      FWorkBuffer.Text := FOutputBuffer[i];
      FOutputBuffer.Objects[i] := TObject(1);

      j := 0;
      while j < FWorkBuffer.Count do
      begin
        // Split long lines in our workbuffer into shorter ones
        if Wrapped and (Length(FWorkBuffer[j]) > LineLen) then
        begin
          FWorkBuffer.Insert(j + 1, Copy(FWorkBuffer[j], LineLen + 1,
                             Length(FWorkBuffer[j])));
          FWorkBuffer[j] := Copy(FWorkBuffer[j], 1, LineLen);
        end;

        // Overwrite/insert individual lines into our output buffer
        if j = 0 then FOutputBuffer[i] := FWorkBuffer[j]
                 else FOutputBuffer.InsertObject(i + j, FWorkBuffer[j], TObject(1));
        Inc(j);
      end;
    end;

    FDisplayBuffer.Add(FOutputBuffer[i]);
    Inc(LineCount); Inc(i);

    // If our output pushed past the maximum allowed number of lines then scroll
    if LineCount > CONSOLE_LINES then
    begin
      // Determine scroll speed - go faster if there's loads of text waiting
      ScrollSpeed := 1;
      if (FOutputBuffer.Count - i) > 5 then ScrollSpeed := 2;
      if (FOutputBuffer.Count - i) > 10 then ScrollSpeed := 4;
      if (FOutputBuffer.Count - i) > 30 then ScrollSpeed := 8;
      if not FScrolling then Inc(FLastLine, ScrollSpeed);

      Break; // no point adding any more lines - they won't be seen
    end;
  end;

  // Return display buffer as a single string
  if Wrapped then Result := FDisplayBuffer.Text
             else Result := FOutputBuffer.Text
end;

function TAHMConsoleManager.GetWatchesEnabled: Boolean;
begin
  Result := FWatches <> [];
end;

function TAHMConsoleManager.GetWatchesOutput: String;
var
  i: Integer;
begin
  // Build watch output
  Result := '';
  if wtFPS in FWatches then
    Result := Result + 'FPS: ' + IntToStr(TimerInstance.ActualFPS) + sLineBreak;
  if wtFrameDur in FWatches then
    Result := Result + IntToStr(TimerInstance.AverageFrameTime) + ' ms' + sLineBreak;
  if wtMeta in FWatches then
  begin
    FWorkBuffer.Clear;
    for i := 0 to Pred(FMetaWatches.Count) do
      ApplicationManagerInstance.MetaDataManager.ListMetaValues(FWorkBuffer, FMetaWatches[i]);
    Result := Result + FWorkBuffer.Text;
  end;
  if wtProfile in FWatches then
    with TimerInstance do
      for i := 0 to Pred(ProfilingCount) do
        Result := Result + ProfilingNames[i] + ': ' + IntToStr(ProfilingTimes[i]) + ' ms' + sLineBreak;
  if wtDebug in FWatches then
    Result := Result + FDebugStrings.Text;
end;

function TAHMConsoleManager.GetDiagnosticText: String;
var
  sDiagText: String;
  Mem: TMemoryStatus;
  i: Integer;
begin
  // Construct diagnostic text
  sDiagText := 'mediate diagnostics:' + sLineBreak + sLineBreak;

  // Frame rate/duration
  sDiagText := sDiagText + 'Average FPS: ' + IntToStr(TimerInstance.ActualFPS) + sLineBreak;
  sDiagText := sDiagText + 'Average frame duration: ' +
               IntToStr(TimerInstance.AverageFrameTime) + ' ms' + sLineBreak;
  sDiagText := sDiagText + 'Current frame duration: ' +
               IntToStr(TimerInstance.CurrentFrameTime) + ' ms' + sLineBreak;

  // Application profiling
  sDiagText := sDiagText + sLineBreak + 'Profiling measurements: ' + sLineBreak;
  with TimerInstance do
  for i := 0 to Pred(ProfilingCount) do
    sDiagText := sDiagText + ProfilingNames[i] + ': ' + IntToStr(ProfilingTimes[i]) + ' ms' + sLineBreak;

  // Video memory usage
  sDiagText := sDiagText + sLineBreak + 'DirectX fonts allocated: ' +
               IntToStr(ScreenInstance.Viewport.Canvas.FontCount) + sLineBreak +
               'DirectX textures allocated: ' +
               IntToStr(ScreenInstance.Viewport.Canvas.GraphicCount) + sLineBreak;

  // DirectX diagnostics
  sDiagText := sDiagText + sLineBreak + 'DirectX diagnostic check: ' +
               ScreenInstance.Viewport.Canvas.DiagnosticMessage;

  // Application memory usage (well, memory remaining really)
  FillChar(Mem, 0, SizeOf(TMemoryStatus));
  Mem.dwLength := SizeOf(TMemoryStatus);
  GlobalMemoryStatus(Mem);
  sDiagText := sDiagText + sLineBreak + 'Memory available - Page file: ' +
               IntToStr(Mem.dwAvailPageFile div 1024) + 'K, Physical: ' +
               IntToStr(Mem.dwAvailPhys div 1024) + 'K' + sLineBreak;


  Result := sDiagText;
end;

procedure TAHMConsoleManager.ScrollUp;
begin
  // Can only scroll if we have more than screen
  if FOutputBuffer.Count < CONSOLE_LINES then Exit;
  FScrolling := True;

  if FLastLine > CONSOLE_PAGE_SIZE then Dec(FLastLine, CONSOLE_PAGE_SIZE)
                                   else FLastLine := 0;
end;

procedure TAHMConsoleManager.ScrollDown;
begin
  // Can only scroll back down if we have scrolled up
  if not FScrolling then Exit;

  if (FLastLine + CONSOLE_LINES + CONSOLE_PAGE_SIZE) < FOutputBuffer.Count then
    Inc(FLastLine, CONSOLE_PAGE_SIZE)
  else
    ScrollCancel;
end;

procedure TAHMConsoleManager.ScrollCancel;
begin
  // Turn off any scrolling (if it's on)
  if FScrolling then
  begin
    FScrolling := False;

    // Position output buffer at end of output log
    if FOutputBuffer.Count > CONSOLE_LINES then
      FLastLine := FOutputBuffer.Count - CONSOLE_LINES
    else
      FLastLine := 0;
  end;
end;

procedure TAHMConsoleManager.ClearOutput;
begin
  FOutputBuffer.Clear;
  FLastLine := 0;
end;

procedure TAHMConsoleManager.Output(Text: String);
begin
  // Parse embedded CR/LF's into separate lines in output buffer
  FWorkBuffer.Text := Text;
  FOutputBuffer.AddStrings(FWorkBuffer);
end;

procedure TAHMConsoleManager.ShowHelp;
begin
  Output(CONSOLE_MSG_HELP);
end;

procedure TAHMConsoleManager.ShowDiagnostics;
begin
  Output(GetDiagnosticText);
end;

procedure TAHMConsoleManager.ShowLog;
begin
  if Params.Count = 0 then
    Output(CONSOLE_MSG_LOG)
  else
  begin
    // Show usage if an invalid logging level entered
    LoggingInstance.LoggingLevelAsString := UpperCase(Params[0]);
    if LoggingInstance.LoggingLevelAsString <> UpperCase(Params[0]) then Output(CONSOLE_MSG_LOG);
  end;
  Output('Current logging level is ' + LoggingInstance.LoggingLevelAsString);
end;

procedure TAHMConsoleManager.ShowVersion;
begin
  Output(ApplicationName + ' version ' + ApplicationVersion);
end;

procedure TAHMConsoleManager.ShowPreferences;
begin
  FWorkBuffer.Clear;
  ApplicationManagerInstance.PreferencesManager.SaveAsXML(FWorkBuffer);
  Output(FWorkBuffer.Text);
end;

procedure TAHMConsoleManager.ShowMeta(Params: TStrings);
var
  Prefix: String;
begin
  Prefix := '';
  if Params.Count = 0 then Output(CONSOLE_MSG_META)
                      else Prefix := Params[0];

  FWorkBuffer.Clear;
  if Prefix = '' then FWorkBuffer.Add('Current metadata values (root):')
                 else FWorkBuffer.Add('Current metadata values (' + Prefix + '):');
  ApplicationManagerInstance.MetaDataManager.ListMetaValues(FWorkBuffer, Prefix);
  Output(FWorkBuffer.Text);
end;

procedure TAHMConsoleManager.ShowObjects(Params: TStrings);
begin
  if (Params.Count <> 1) or (GetObjectList(Params[0]) = nil) then
    Output(CONSOLE_MSG_LIST)
  else
  begin
    FWorkBuffer.Text := 'Available ' + Lowercase(Params[0]) + 's:';
    GetObjectList(Params[0]).ListContents(FWorkBuffer);
    Output(FWorkBuffer.Text);
  end;
end;

procedure TAHMConsoleManager.DumpObject(Params: TStrings);
var
  ListObj: TAHMUIObject;
begin
  if (Params.Count <> 2) or (GetObjectList(Params[0]) = nil) then
    Output(CONSOLE_MSG_DUMP)
  else
  begin
    FWorkBuffer.Clear;
    ListObj := GetObjectList(Params[0]).GetObject(Params[1]);
    if ListObj = nil then FWorkBuffer.Text := 'Unknown object: ' + Params[1]
                     else ListObj.SaveAsXML(FWorkBuffer);
    Output(FWorkBuffer.Text);
  end;
end;

function TAHMConsoleManager.GetObjectList(ObjectType: String): TAHMUIObjectList;
begin
  // Case insensitive lookup
  ObjectType := UpperCase(ObjectType);

  // Return appropriate UIObject list
  with ApplicationManagerInstance, ApplicationManagerInstance.ThemeManager do
    if      ObjectType = 'COLOUR' then Result := VisualDescriptor.ColourList
    else if ObjectType = 'FILL' then Result := VisualDescriptor.FillList
    else if ObjectType = 'PLASMA' then Result := VisualDescriptor.PlasmaList
    else if ObjectType = 'FONT' then Result := VisualDescriptor.FontList
    else if ObjectType = 'BITMAP' then Result := VisualDescriptor.BitmapList
    else if ObjectType = 'BACKGROUND' then Result := VisualDescriptor.BackgroundList
    else if ObjectType = 'FOREGROUND' then Result := VisualDescriptor.ForegroundList
    else if ObjectType = 'SHAPE' then Result := VisualDescriptor.ShapeList
    else if ObjectType = 'CAPTION' then Result := VisualDescriptor.CaptionList
    else if ObjectType = 'MEDIAWINDOW' then Result := VisualDescriptor.MediaWindowList
    else if ObjectType = 'BAR' then Result := VisualDescriptor.BarList
    else if ObjectType = 'BUTTON' then Result := VisualDescriptor.ButtonList
    else if ObjectType = 'MEDIAGRID' then Result := VisualDescriptor.MediaGridList
    else if ObjectType = 'TEXTEDIT' then Result := VisualDescriptor.TextEditList
    else if ObjectType = 'EFFECT' then Result := VisualDescriptor.EffectList
    else if ObjectType = 'PLUGINWINDOW' then Result := VisualDescriptor.PluginWindowList
    else if ObjectType = 'FUNCTION' then Result := FunctionDescriptor.FunctionList
    else if ObjectType = 'EXPRESSION' then Result := InterfaceDescriptor.ExpressionList
    else if ObjectType = 'MACRO' then Result := InterfaceDescriptor.MacroList
    else if ObjectType = 'ACTION' then Result := InterfaceDescriptor.ActionList
    else if ObjectType = 'TIMER' then Result := InterfaceDescriptor.TimerList
    else if ObjectType = 'METADATA' then Result := InterfaceDescriptor.MetaList
    else if ObjectType = 'PLACE' then Result := InterfaceDescriptor.PlaceList
    else if ObjectType = 'ANIMATOR' then Result := InterfaceDescriptor.AnimatorList
    else if ObjectType = 'LAYOUT' then Result := InterfaceDescriptor.LayoutList
    else if ObjectType = 'CONTROL' then Result := InterfaceDescriptor.ControlList
    else if ObjectType = 'CONTAINER' then Result := InterfaceDescriptor.ContainerList
    else if ObjectType = 'SCREEN' then Result := InterfaceDescriptor.ScreenList
    else if ObjectType = 'SOUND' then Result := AudioDescriptor.SoundList
    else if ObjectType = 'LANGUAGE' then Result := LanguageDescriptor.LanguageManager.Languages
    else Result := nil;
end;

procedure TAHMConsoleManager.ShowUptime;
begin
  with TimerInstance do
  begin
    Output('mediate running since ' + FormatDateTime('hh:nn:ss', StartedTime) +
                             ' on ' + FormatDateTime('dddd d mmmm', StartedDate));
    Output('(' + DurationToString(TimerInstance.RunningTime) + ')');
  end;
end;

procedure TAHMConsoleManager.ShowUnknown;
begin
  Output(CONSOLE_MSG_UNKNOWN_CMD);
end;

procedure TAHMConsoleManager.CmdFunction(Params: TStrings);
var
  MyFunction: TAHMFunction;
  Target, Param: String;
  FnResult: TAHMFunctionResult;
begin
  // Validate parameters for function call
  if Params.Count = 0 then
  begin
    Output(CONSOLE_MSG_FN);
    Exit;
  end;
  if Params.Count > 1 then Target := Params[1] else Target := '';
  if Params.Count > 2 then Param := Params[2] else Param := '';

  // Lookup corresponding function object
  MyFunction := ApplicationManagerInstance.FunctionDescriptor.FunctionList.GetFunction(Params[0]);
  if not Assigned(MyFunction) then
  begin
    Output('Unknown function!');
    Exit;
  end;

  // Execute function but catch any exceptions
  FnResult := frFailed;
  try
    if Target = '' then
      FnResult := MyFunction.Execute
    else
      FnResult := MyFunction.Execute(Target, Param);
  except
    on E:Exception do
      Output('Function caused exception: ' + E.Message);
  end;

  // Report status
  if Target <> '' then Target := ' ' + Target;
  if Param <> '' then Param := ' ' + Param;
  case FnResult of
    frSuccess: Output(Params[0] + Target + Param + ' : Okay');
    frFailed: Output(Params[0] + Target + Param + ' : Failed');
    frPartial: Output(Params[0] + Target + Param + ' : Working');
  end;
end;

procedure TAHMConsoleManager.CmdClear;
begin
  ClearOutput;
end;

procedure TAHMConsoleManager.CmdExit;
begin
  ApplicationManagerInstance.UIManager.DisplayDiagnostics := False;
end;

procedure TAHMConsoleManager.CmdRestart;
begin
  // Disable this command in theme designer
  if ApplicationManagerInstance.DesignerMode then
    Output('Restart command not allowed in designer.')
  else
    FunctionsInstance.Restart;
end;

procedure TAHMConsoleManager.CmdQuit;
begin
  // Disable this command in theme designer
  if ApplicationManagerInstance.DesignerMode then
    Output('Quit command not allowed in designer.')
  else
    FunctionsInstance.Quit;
end;

procedure TAHMConsoleManager.CmdBye;
begin
  // Disable this command in theme designer
  if ApplicationManagerInstance.DesignerMode then
    Output('Bye command not allowed in designer.')
  else
    Application.Terminate;
end;

procedure TAHMConsoleManager.CmdExample(Params: TStrings);
var
  i: Integer;
begin
  // Example command making use of passed parameters
  Output('Example command. You passed ' + IntToStr(Params.Count) + ' parameters');
  for i := 0 to Pred(Params.Count) do
    Output('Param[' + IntToStr(i) + '] = ' + Params[i]);
end;

procedure TAHMConsoleManager.AddWatch(WatchType: TAHMWatchType);
begin
  Include(FWatches, WatchType);
end;

procedure TAHMConsoleManager.AddWatch(WatchType: TAHMWatchType; Params: TStrings);
var
  i: Integer;
begin
  Include(FWatches, WatchType);

  if (WatchType = wtMeta) then
    for i := 0 to Pred(Params.Count) do
      if Params[i] <> '' then FMetaWatches.Add(Params[i]);
end;

procedure TAHMConsoleManager.RemoveWatch(WatchType: TAHMWatchType);
begin
  Exclude(FWatches, WatchType);
end;

procedure TAHMConsoleManager.RemoveWatch(WatchType: TAHMWatchType; Params: TStrings);
var
  i, j: Integer;
begin
  Exclude(FWatches, WatchType);

  if (WatchType = wtMeta) then
  begin
    for i := 0 to Pred(Params.Count) do
    begin
      j := FMetaWatches.IndexOf(Params[i]);
      if j >= 0 then FMetaWatches.Delete(j);
    end;

    // If we specified a subset of meta watches then leave rest on display
    if (Params.Count > 0) and (FMetaWatches.Count > 0) then
      Include(FWatches, wtMeta);
  end;
end;

function TAHMConsoleManager.ProcessKeyPress(Key: Word; Shift: TShiftState): Boolean;
begin
  Result := FCommandLine.ProcessKeyPress(Key, Shift);
end;

procedure TAHMConsoleManager.ProcessCommand(Command: String);
begin
  FCommandLine.Text := Command;
  FCommandLine.Enter;
end;

end.
